/**
 *
 */
package edu.tjut.tjutcommunity.recommend.contentbasedrecommend;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import edu.tjut.tjutcommunity.entity.Post;
import edu.tjut.tjutcommunity.mapper.PostMapper;
import edu.tjut.tjutcommunity.recommend.algorithms.RecommendAlgorithm;
import edu.tjut.tjutcommunity.recommend.algorithms.RecommendKit;
import org.ansj.app.keyword.Keyword;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * @author tjut
 * @email
 * @github
 * @date 基于内容的推荐 Content-Based
 *
 *       思路：提取抓取进来的新闻的关键词列表（tf-idf），与每个用户的喜好关键词列表，做关键词相似度计算，取最相似的N个新闻推荐给用户。
 *
 *       Procedure: 1、Every time that the recommendation is started up(according
 *       to quartz framework), the current day's coming in news will be
 *       processed by class TF-IDF's getTFIDF() method to obtain their key words
 *       list.And then the system go over every user and calculate the
 *       similarity between every news's key words list with user's preference
 *       list.After that, rank the news according to the similarities and
 *       recommend them to users.
 */
@Component
public class ContentBasedRecommender implements RecommendAlgorithm {
    public static final Logger logger = Logger.getLogger(ContentBasedRecommender.class);

    // TFIDF提取关键词数
    @Value("${recommend.TFIDFKeywordsNum}")
    private int KEY_WORDS_NUM;

    final
    UserPrefRefresher userPrefRefresher;

    final
    PostMapper postMapper;


    // 推荐帖子数
    @Value("${recommend.CBRecNum}")
    private int N;

    public ContentBasedRecommender(UserPrefRefresher userPrefRefresher, PostMapper postMapper) {
        this.userPrefRefresher = userPrefRefresher;
        this.postMapper = postMapper;
    }


    /**
     * 获得比时间近的帖子
     *
     * @param inRecDate
     * @return
     */
    public List<Post> getRecPost(String inRecDate) {
        QueryWrapper<Post> qw = new QueryWrapper<>();
        qw.gt("update_time", inRecDate);
        return postMapper.selectList(qw);
    }

    /**
     * 根据传入的用户列表生成推荐，
     * @param users
     */
    @Override
    public void recommend(List<Integer> users) {
        try {
            int count = 0;
            // 首先进行用户喜好关键词列表的衰减更新+用户当日历史浏览记录的更新
            userPrefRefresher.refresh(users);
            // 帖子及对应关键词列表的Map
            HashMap<Integer, List<Keyword>> newsKeyWordsMap = new HashMap<Integer, List<Keyword>>();
            HashMap<Integer, Integer> newsModuleMap = new HashMap<Integer, Integer>();

            // 获取用户喜好关键词列表
            HashMap<Integer, CustomizedHashMap<Integer, CustomizedHashMap<String, Double>>> userPrefListMap = RecommendKit
                    .getUserPrefListMap(users);

            List<Post> postList = getRecPost(RecommendKit.getInRecDate());
            for (Post post : postList) {
                newsKeyWordsMap.put(post.getPid(), TFIDF.getTFIDF(post.getTitle(), post.getDescription(), KEY_WORDS_NUM));
                newsModuleMap.put(post.getPid(), post.getType());
            }

            for (Integer userId : users) {
                Map<Integer, Double> tempMatchMap = new HashMap<Integer, Double>();
                for (Integer newsId : newsKeyWordsMap.keySet()) {
                    int moduleId = newsModuleMap.get(newsId);
                    if (null != userPrefListMap.get(userId).get(moduleId))
                        tempMatchMap.put(newsId,
                                getMatchValue(userPrefListMap.get(userId).get(moduleId), newsKeyWordsMap.get(newsId)));
                }
                // 去除匹配值为0的项目
                removeZeroItem(tempMatchMap);
                if (!(tempMatchMap.toString().equals("{}"))) {
                    tempMatchMap = sortMapByValue(tempMatchMap);
                    Set<Integer> toBeRecommended = tempMatchMap.keySet();
                    //过滤掉已经推荐过的帖子
                    RecommendKit.filterReccedPosts(toBeRecommended, userId);
                    //过滤掉用户已经看过的帖子
                    RecommendKit.filterBrowsedPosts(toBeRecommended, userId);
                    //如果可推荐新闻数目超过了系统默认为CB算法设置的单日推荐上限数（N），则去掉一部分多余的可推荐帖子，剩下的N个帖子才进行推荐
                    if (toBeRecommended.size() > N) {
                        RecommendKit.removeOverPosts(toBeRecommended, N);
                    }
                    RecommendKit.insertRecommend(userId, toBeRecommended.iterator(), RecommendAlgorithm.CB);
                    count += toBeRecommended.size();
                }
            }
            System.out.println("CB has contributed " + (count / users.size()) + " recommending news on average");
            System.out.println("CB finished at " + new Date());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获得用户的关键词列表和帖子关键词列表的匹配程度
     *
     * @return
     */
    private double getMatchValue(CustomizedHashMap<String, Double> map, List<Keyword> list) {
        Set<String> keywordsSet = map.keySet();
        double matchValue = 0;
        for (Keyword keyword : list) {
            if (keywordsSet.contains(keyword.getName())) {
                matchValue += keyword.getScore() * map.get(keyword.getName());
            }
        }
        return matchValue;
    }

    private void removeZeroItem(Map<Integer, Double> map) {
        HashSet<Integer> toBeDeleteItemSet = new HashSet<Integer>();
        Iterator<Integer> ite = map.keySet().iterator();
        while (ite.hasNext()) {
            Integer newsId = ite.next();
            if (map.get(newsId) <= 0) {
                toBeDeleteItemSet.add(newsId);
            }
        }
        for (Integer item : toBeDeleteItemSet) {
            map.remove(item);
        }
    }

    /**
     * 使用 Map按value进行排序
     * @param
     * @param oriMap
     * @return
     */
    public static Map<Integer, Double> sortMapByValue(Map<Integer, Double> oriMap) {
        if (oriMap == null || oriMap.isEmpty()) {
            return null;
        }
        Map<Integer, Double> sortedMap = new LinkedHashMap<Integer, Double>();
        List<Map.Entry<Integer, Double>> entryList = new ArrayList<Map.Entry<Integer, Double>>(
                oriMap.entrySet());
        Collections.sort(entryList, new MapValueComparator());

        Iterator<Map.Entry<Integer, Double>> iter = entryList.iterator();
        Map.Entry<Integer, Double> tmpEntry = null;
        while (iter.hasNext()) {
            tmpEntry = iter.next();
            sortedMap.put(tmpEntry.getKey(), tmpEntry.getValue());
        }
        return sortedMap;
    }
}
